51. 数据框缺失值处理

本章概要

  • 学习材料:含价格、成交量和缺失模式的时间序列表。
  • 本章任务:运行 lst-detect-missing-statslst-stock-suspension-strategies,比较原始/前填/插值口径。
  • 完成后你将得到:缺失计数、处理后表和三种收益率对照。
  • 自我检查:检查原始缺失标识保留且ffill结果无缺失;停牌语义未知时先检查原因。
  • 拓展练习:把双口径缺失检查拓展应用到另一中国股票。

本章概览:为什么要处理缺失值?

缺失值(Missing Values)是数据分析中无法回避的普遍问题。

  • 金融市场中尤为常见:停牌、新股上市、节假日休市等
  • 简单忽略会导致统计偏倚信息损失
  • 大多数算法(回归、神经网络等)无法直接处理缺失值

本章核心任务:学会识别、诊断和处理数据框中的缺失值。

金融数据缺失的典型场景

场景 说明
停牌事件 股票因重大事项停牌,期间无交易数据
新股上市 上市前无历史数据,产生”左截断”问题
节假日效应 A股春节、国庆休市,海外市场继续交易
数据源故障 数据供应商技术问题导致部分数据不可用
公司退市 退市公司被剔除,历史数据不再维护

不能简单忽略缺失值的三大原因

  1. 统计偏倚:若缺失非随机,删除会导致样本有偏
    • 例:删除退市公司数据会高估股票真实收益
  2. 信息损失:A股约5000只股票,样本有限,每一笔数据都珍贵
  3. 算法要求:线性回归、神经网络等均无法直接处理缺失值

Pandas中的缺失值表示:NaN

Pandas使用 NaN(Not a Number)表示数值型缺失值。

NaN的特殊性质

  • NaN != NaN(自反性不成立)
  • NaN + x = NaN(吸收律:任何运算结果仍为NaN)
  • 永远不要用 == 判断NaN,应使用 pd.isna()np.isnan()

NaN的数学特性演示

Listing 1: NaN的数学特性演示
展开实现代码
import numpy as np
import pandas as pd

nan_value = np.nan

# NaN的比较特性
print('NaN的比较特性:')
print(f'NaN == NaN: {nan_value == nan_value}')   # False!
print(f'NaN != NaN: {nan_value != nan_value}')   # True!
print(f'NaN is NaN: {nan_value is nan_value}')   # True

# NaN的运算特性(吸收律)
print('\nNaN的运算特性:')
print(f'NaN + 100: {nan_value + 100}')   # NaN
print(f'NaN * 2: {nan_value * 2}')       # NaN
NaN的比较特性:
NaN == NaN: False
NaN != NaN: True
NaN is NaN: True

NaN的运算特性:
NaN + 100: nan
NaN * 2: nan

缺失值检测方法

Listing 2: 缺失值的检测方法
展开实现代码
import pandas as pd
import numpy as np

# 创建包含缺失值的示例数据
data = {
    '股票代码': ['600519.SH', '000858.SZ', '600036.SH', '601318.SH', '000001.SZ'],
    '收盘价': [1850.0, np.nan, 45.2, 52.8, np.nan],
    '涨跌幅': [0.05, -0.02, np.nan, -0.01, 0.03],
    '成交量': [1200, 3500, np.nan, 5600, 2800]
}
df = pd.DataFrame(data)
print('原始数据:')
print(df)
原始数据:
        股票代码     收盘价   涨跌幅     成交量
0  600519.SH  1850.0  0.05  1200.0
1  000858.SZ     NaN -0.02  3500.0
2  600036.SH    45.2   NaN     NaN
3  601318.SH    52.8 -0.01  5600.0
4  000001.SZ     NaN  0.03  2800.0

统计每列的缺失值数量与比例

Listing 3: 缺失值统计
展开实现代码
# 每列缺失值数量
print('每列缺失值数量:')
print(df.isna().sum())

# 每列缺失值比例
print('\n每列缺失值比例(%):')
print((df.isna().sum() / len(df) * 100).round(2))

# 完整行数统计
complete = df.dropna()
print(f'\n完整行数: {len(complete)} / {len(df)}')
每列缺失值数量:
股票代码    0
收盘价     2
涨跌幅     1
成交量     1
dtype: int64

每列缺失值比例(%):
股票代码     0.0
收盘价     40.0
涨跌幅     20.0
成交量     20.0
dtype: float64

完整行数: 2 / 5

缺失值模式分析

理解哪些变量倾向于一起缺失,有助于诊断缺失原因。

Listing 4: 缺失值模式分析
def missing_correlation(df):
    """计算变量间的缺失模式相关性"""
    return df.isna().corr()

miss_corr = missing_correlation(df)
print('缺失模式相关性:')
print(miss_corr.round(2))
# 相关系数接近1:两变量倾向于同时缺失
缺失模式相关性:
      股票代码   收盘价   涨跌幅   成交量
股票代码   NaN   NaN   NaN   NaN
收盘价    NaN  1.00 -0.41 -0.41
涨跌幅    NaN -0.41  1.00  1.00
成交量    NaN -0.41  1.00  1.00

删除策略概览

场景 推荐策略 理由
缺失<5%且随机 dropna() 信息损失小
关键指标缺失 dropna(subset=[...]) 关键指标不可缺
缺失>50% 考虑删除该变量 信息太少,插补不可靠

dropna的多种删除策略

Listing 5: dropna的多种删除策略
展开实现代码
# 策略1:删除包含任何缺失值的行
df_any = df.dropna(how='any')
print('删除任何缺失值的行:')
print(df_any)

# 策略2:删除全部为缺失值的行
df_all = df.dropna(how='all')
print('\n删除全部为缺失值的行:')
print(df_all)

# 策略3:删除特定列中有缺失值的行
df_subset = df.dropna(subset=['收盘价', '涨跌幅'])
print('\n在收盘价或涨跌幅有缺失的行被删除:')
print(df_subset)
删除任何缺失值的行:
        股票代码     收盘价   涨跌幅     成交量
0  600519.SH  1850.0  0.05  1200.0
3  601318.SH    52.8 -0.01  5600.0

删除全部为缺失值的行:
        股票代码     收盘价   涨跌幅     成交量
0  600519.SH  1850.0  0.05  1200.0
1  000858.SZ     NaN -0.02  3500.0
2  600036.SH    45.2   NaN     NaN
3  601318.SH    52.8 -0.01  5600.0
4  000001.SZ     NaN  0.03  2800.0

在收盘价或涨跌幅有缺失的行被删除:
        股票代码     收盘价   涨跌幅     成交量
0  600519.SH  1850.0  0.05  1200.0
3  601318.SH    52.8 -0.01  5600.0

缺失值填充方法:常数填充

Listing 6: 使用常数值填充缺失值
# 方法1:用0填充(适合成交量等,不适合价格)
df_zero = df.fillna(0)
print('用0填充:')
print(df_zero)
用0填充:
        股票代码     收盘价   涨跌幅     成交量
0  600519.SH  1850.0  0.05  1200.0
1  000858.SZ     0.0 -0.02  3500.0
2  600036.SH    45.2  0.00     0.0
3  601318.SH    52.8 -0.01  5600.0
4  000001.SZ     0.0  0.03  2800.0

均值填充与中位数填充

Listing 7: 均值和中位数填充
展开实现代码
# 方法2:用均值填充
mean_price = df['收盘价'].mean()
df_mean = df.fillna({'收盘价': mean_price})
print(f'用均值填充(均值={mean_price:.2f}):')
print(df_mean)

# 方法3:用中位数填充(对异常值更稳健)
median_price = df['收盘价'].median()
df_median = df.fillna({'收盘价': median_price})
print(f'\n用中位数填充(中位数={median_price:.2f}):')
print(df_median)
用均值填充(均值=649.33):
        股票代码          收盘价   涨跌幅     成交量
0  600519.SH  1850.000000  0.05  1200.0
1  000858.SZ   649.333333 -0.02  3500.0
2  600036.SH    45.200000   NaN     NaN
3  601318.SH    52.800000 -0.01  5600.0
4  000001.SZ   649.333333  0.03  2800.0

用中位数填充(中位数=52.80):
        股票代码     收盘价   涨跌幅     成交量
0  600519.SH  1850.0  0.05  1200.0
1  000858.SZ    52.8 -0.02  3500.0
2  600036.SH    45.2   NaN     NaN
3  601318.SH    52.8 -0.01  5600.0
4  000001.SZ    52.8  0.03  2800.0

运行前预测|平台任务1:检测缺失值

  • 输入预测:运行前先写出 index_bric 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到index_bric 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务1:检测缺失值”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务1:检测缺失值

展开完整代码(投影默认折叠)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
index_bric = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220820/xlsx/1560916966116974592.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据
print(index_bric.isnull().any())    #查找每一列是否存在缺失值(用isnull函数)

print(index_bric.isna().any())     #查找每一列是否存在缺失值(用isna函数)
print(index_bric[index_bric.isnull().values==True]) #查找存在缺失值所在的行

任务复盘|平台任务1:检测缺失值

运行后核对:核对 index_bric 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把输入表替换为本地中国上市公司数据的同结构子集;指出必须保持的字段、数据类型和质量检查。

运行前预测|平台任务2:删除缺失值行

  • 输入预测:运行前先写出 index_bricindex_bric_dropna 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到index_bric_dropna 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务2:删除缺失值行”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务2:删除缺失值行

展开完整代码(投影默认折叠)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
index_bric = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220820/xlsx/1560916966116974592.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据

index_bric_dropna = index_bric.dropna()  #删除存在缺失值的行数并创建一个新的数据框
print(index_bric_dropna.isnull().any())  # 输出缺失值检查结果
print(index_bric.shape)          #查看原数据框的形状参数
print(index_bric_dropna.shape)      #查看新数据框的形状参数

任务复盘|平台任务2:删除缺失值行

运行后核对:核对 index_bricindex_bric_dropna 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把输入表替换为本地中国上市公司数据的同结构子集;指出必须保持的字段、数据类型和质量检查。

运行前预测|平台任务3:前向补齐

  • 输入预测:运行前先写出 index_bricindex_bric_ffill 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到“2019-06-04”相关结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务3:前向补齐”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务3:前向补齐

展开完整代码(投影默认折叠)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
index_bric = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220820/xlsx/1560916966116974592.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据

index_bric_ffill = index_bric.fillna(method="ffill")   #向前补齐
print(index_bric_ffill.isnull().any())  # 输出缺失值检查结果
print(index_bric_ffill.loc["2019-06-04":"2019-06-12"])  # 输出2019-06-04

任务复盘|平台任务3:前向补齐

运行后核对:核对 index_bricindex_bric_ffill 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把输入表替换为本地中国上市公司数据的同结构子集;指出必须保持的字段、数据类型和质量检查。

运行前预测|平台任务4:后向补齐

  • 输入预测:运行前先写出 index_bricindex_bric_bfill 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到“2019-06-04”相关结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务4:后向补齐”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务4:后向补齐

展开完整代码(投影默认折叠)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
index_bric = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220820/xlsx/1560916966116974592.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据

index_bric_bfill = index_bric.fillna(method="bfill")   #向后补齐
print(index_bric_bfill.isnull().any())  # 输出缺失值检查结果
print(index_bric_bfill.loc["2019-06-04":"2019-06-13"])  # 输出2019-06-04

任务复盘|平台任务4:后向补齐

运行后核对:核对 index_bricindex_bric_bfill 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把输入表替换为本地中国上市公司数据的同结构子集;指出必须保持的字段、数据类型和质量检查。

插值法填充缺失值

Listing 8: 插值法填充缺失值
展开实现代码
import numpy as np
import pandas as pd

# 创建包含缺失值的时间序列数据
dates = pd.date_range('2024-01-01', periods=10)
ts_data = pd.DataFrame({
    '日期': dates,
    '价格': [10.5, np.nan, np.nan, 11.2, np.nan,
             11.8, np.nan, np.nan, 12.5, 12.8]
})

# 线性插值:在两个已知点之间画直线
ts_linear = ts_data.copy()
ts_linear['价格_线性插值'] = ts_linear['价格'].interpolate(method='linear')
print('线性插值:')
print(ts_linear)
线性插值:
          日期    价格    价格_线性插值
0 2024-01-01  10.5  10.500000
1 2024-01-02   NaN  10.733333
2 2024-01-03   NaN  10.966667
3 2024-01-04  11.2  11.200000
4 2024-01-05   NaN  11.500000
5 2024-01-06  11.8  11.800000
6 2024-01-07   NaN  12.033333
7 2024-01-08   NaN  12.266667
8 2024-01-09  12.5  12.500000
9 2024-01-10  12.8  12.800000

金融应用案例:停牌数据处理

Listing 9: 股票停牌数据的处理
展开实现代码
import pandas as pd
import numpy as np

# 模拟招商银行停牌场景
dates = pd.date_range('2024-01-01', periods=10)
suspension_data = pd.DataFrame({
    '日期': dates,
    '收盘价': [10.5, 10.8, 11.0, np.nan, np.nan,
               np.nan, np.nan, np.nan, 11.5, 11.7]
})
suspension_data['是否停牌'] = suspension_data['收盘价'].isna()
print('停牌数据:')
print(suspension_data)
停牌数据:
          日期   收盘价   是否停牌
0 2024-01-01  10.5  False
1 2024-01-02  10.8  False
2 2024-01-03  11.0  False
3 2024-01-04   NaN   True
4 2024-01-05   NaN   True
5 2024-01-06   NaN   True
6 2024-01-07   NaN   True
7 2024-01-08   NaN   True
8 2024-01-09  11.5  False
9 2024-01-10  11.7  False

停牌数据:两种处理策略

Listing 10: 停牌数据处理策略对比
展开实现代码
# 策略1:前向填充(金融分析标准方法)
df_strategy1 = suspension_data.copy()
df_strategy1['收盘价'] = df_strategy1['收盘价'].fillna(method='ffill')
print('策略1 - 前向填充:')
print(df_strategy1)

# 策略2:删除停牌行
df_strategy2 = suspension_data.dropna(subset=['收盘价'])
print('\n策略2 - 删除停牌行:')
print(df_strategy2)
策略1 - 前向填充:
          日期   收盘价   是否停牌
0 2024-01-01  10.5  False
1 2024-01-02  10.8  False
2 2024-01-03  11.0  False
3 2024-01-04  11.0   True
4 2024-01-05  11.0   True
5 2024-01-06  11.0   True
6 2024-01-07  11.0   True
7 2024-01-08  11.0   True
8 2024-01-09  11.5  False
9 2024-01-10  11.7  False

策略2 - 删除停牌行:
          日期   收盘价   是否停牌
0 2024-01-01  10.5  False
1 2024-01-02  10.8  False
2 2024-01-03  11.0  False
8 2024-01-09  11.5  False
9 2024-01-10  11.7  False

停牌处理策略比较

策略 优点 缺点
前向填充 简单直观,保持时间连续性 低估真实波动率,延迟反映价格跳跃
删除行 避免虚假数据 破坏时间连续性,丢失信息

金融实践建议

  • 计算收益率时:前向填充是标准方法
  • 计算波动率时:建议删除停牌行

不同插值方法的比较

Listing 11: 不同插值方法的比较
import numpy as np
import pandas as pd

# 创建测试数据(带噪声的正弦波)
x = np.linspace(0, 10, 20)
y_true = np.sin(x) + np.random.normal(0, 0.1, 20)

# 人为制造缺失值
y_missing = y_true.copy()
y_missing[np.random.choice(20, 5, replace=False)] = np.nan

# 线性插值 vs 样条插值
y_linear = pd.Series(y_missing).interpolate(method='linear')
y_spline = pd.Series(y_missing).interpolate(method='cubic')

# 计算MSE
print(f'线性插值MSE: {np.mean((y_true - y_linear)**2):.4f}')
print(f'样条插值MSE: {np.mean((y_true - y_spline)**2):.4f}')
线性插值MSE: 0.0005
样条插值MSE: 0.0046

本章小结

  • 识别缺失值:使用 isna() / isnull() 检测,sum() 统计
  • 删除策略dropna(how='any') / dropna(subset=[...]) 按需选择
  • 常数填充fillna(0) / fillna(均值) / fillna(中位数)
  • 方向填充fillna(method='ffill') 前向 / fillna(method='bfill') 后向
  • 插值法interpolate(method='linear') 线性 / 'cubic' 样条
  • 金融实践:停牌数据推荐使用前向填充

随堂练习

  • 问题 1|需要准备哪些数据?:含价格、成交量和缺失模式的时间序列表。
  • 问题 2|需要完成哪些操作?:运行 lst-detect-missing-statslst-stock-suspension-strategies,比较原始/前填/插值口径。
  • 问题 3|应得到哪些结果?:缺失计数、处理后表和三种收益率对照。
  • 问题 4|怎样确认结果可靠?:检查原始缺失标识保留且ffill结果无缺失;停牌语义未知时先检查原因。
  • 问题 5|换一个情境,怎样继续应用?:把双口径缺失检查拓展应用到另一中国股票。
  • 作答提示:请依次写清所用数据、分析过程、所得结果、核对方法和拓展思考。课程所需数据见前言中的下载入口;教学平台固定题按页面说明完成。

教师参考解答|答案与说明 1

  • 所用数据与字段:时间升序价格列 price:float;缺失含义已确认,不能把停牌与采集故障混为一类。

教师参考解答|代码 1

展开代码(代码区可独立滚动)
import pandas as pd, numpy as np  # 导入表格工具以构造、对齐和核对数据
s=pd.Series([10.,np.nan,np.nan,10.6],index=pd.date_range('2025-01-01',periods=4))  # 生成s以承接当前分析步骤
result=pd.DataFrame({'observed':s,'is_missing':s.isna()})  # 保存处理后的表或函数返回结果
result['ffill']=s.ffill(limit=1); result['linear']=s.interpolate(limit=1)  # 保存处理后的表或函数返回结果
audit={'before':int(s.isna().sum()),'ffill_after':int(result['ffill'].isna().sum()),'linear_after':int(result['linear'].isna().sum())}  # 显式按列名统计两种处理后的剩余缺失
assert result['observed'].isna().equals(result['is_missing'])  # 保存处理后的表或函数返回结果
print(result,audit)  # 记录每一步净值或缺失处理检查

教师参考解答|答案与说明 2

  • 解释答案:缺失处理取决于生成机制和可接受时距;前向填充与线性插值不能跨越未批准的长缺口,原始缺失标记必须保留。
  • 拓展应用答案:拓展应用到日销量时,替换价格序列并预声明最多填1期,比较两种填充后的剩余缺失数,人工核对一个连续缺口。
  • 参考结果:原值、缺失标识、两种受限处理列和前后缺失数。另行核对:不允许处理列覆盖原值;改变 limit 比较敏感性。
  • 常见错误:无限前向填充;停牌收益设0后当观测;插补值冒充真实值。